package process

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"os/signal"
	"path"
	"strconv"
	"syscall"
)

// EnvName Identify the name of the environment variable that is the child process.
const EnvName = "DAEMON"

var PidPath = "/var/run"

type signalHandler func(p *Process)

type Worker interface {
	Name() string
	Start()
	Stop() error
	Restart() error
}

type Process struct {
	worker    Worker
	pid       int
	handlers  map[os.Signal]signalHandler
	file      *os.File // pid file
	pipelines [3]*os.File
}

// NewProcess create an deamon process
func NewProcess(worker Worker) *Process {
	proc := &Process{
		worker:   worker,
		pid:      os.Getpid(),
		handlers: make(map[os.Signal]signalHandler),
	}

	proc.register(os.Interrupt, stopHandler)
	proc.register(syscall.SIGUSR2, restartHandler)

	return proc
}

func (process *Process) register(s os.Signal, handler func(p *Process)) {
	process.handlers[s] = handler
}

func (process *Process) listen() {
	sig := make(chan os.Signal)
	signal.Notify(sig, os.Interrupt, syscall.SIGUSR2)
	for {
		res := <-sig
		if hander, ok := process.handlers[res]; ok {
			hander(process)
		}
	}
}

// SetPipeline  0 -> stdin, 1 -> stdout, 2 -> stderr
func (process *Process) SetPipeline(pipes ...*os.File) {
	if len(pipes) >= 3 {
		pipes = pipes[0:3]
	}
	for index, pipe := range pipes {
		process.pipelines[index] = pipe
	}
}

// Start deamon process
func (process *Process) Start() error {
	flag, err := strconv.Atoi(os.Getenv(EnvName))
	if err != nil {
		flag = 0
	}

	// create pid file
	filename := path.Join(PidPath, process.worker.Name()+".pid")
	f, err := createPid(process.pid, filename)
	if err != nil {
		if f != nil {
			f.Close()
		}
		return err
	}
	process.file = f

	if flag == 1 {
		// 执行子进程
		go process.worker.Start()
		// 监听信号量
		process.listen()
		return nil
	}

	cmd := exec.Command(os.Args[0], os.Args[1:]...)
	cmd.Env = append(os.Environ(), fmt.Sprintf("%s=1", EnvName))
	cmd.Stdin, cmd.Stdout, cmd.Stderr = process.pipelines[0], process.pipelines[1], process.pipelines[2]
	err = cmd.Start()

	if err != nil {
		return err
	}
	return cmd.Process.Release()
}

func (process *Process) readPid() (int, error) {
	filename := path.Join(PidPath, process.worker.Name()+".pid")
	id, err := ioutil.ReadFile(filename)
	if err != nil {
		return 0, err
	}

	pid, err := strconv.Atoi(string(id))
	if err != nil {
		return 0, err
	}
	return pid, nil
}

func (process *Process) Stop() error {
	pid, err := process.readPid()
	if err != nil {
		return err
	}
	p, err := os.FindProcess(pid)

	if err != nil {
		return err
	}
	return p.Signal(os.Interrupt)
}
func (process *Process) Restart() error {
	pid, err := process.readPid()
	if err != nil {
		return err
	}
	p, err := os.FindProcess(pid)
	if err != nil {
		return err
	}
	return p.Signal(syscall.SIGUSR2)
}

func stopHandler(p *Process) {
	removePid(p.file)
	err := p.worker.Stop()
	if err != nil {
		fmt.Println(err)
	}
	os.Exit(0)
}

func restartHandler(p *Process) {
	removePid(p.file)
	var done = make(chan bool)
	go func() {
		err := p.worker.Restart()
		if err != nil {
			fmt.Println(err)
		}
		done <- true
	}()
	os.Unsetenv(EnvName)
	err := p.Start()
	if err != nil {
		fmt.Println(err)
	}
	<-done
	os.Exit(0)
}

func createDir(filePath string) error {
	dir := path.Dir(filePath)
	_, err := os.Stat(dir)
	if err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		err = os.MkdirAll(dir, 0644)
		if err != nil {
			return err
		}
	}
	return nil
}

func createPid(pid int, pidFile string) (*os.File, error) {
	err := createDir(path.Dir(pidFile))
	if err != nil {
		return nil, err
	}
	f, err := os.OpenFile(pidFile, os.O_CREATE|os.O_RDWR, 0644)
	if err != nil {
		return nil, err
	}
	//flock file
	if err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
		return f, err
	}

	f.Truncate(0)
	if err != nil {
		return f, err
	}

	_, err = f.WriteString(strconv.Itoa(pid))
	if err != nil {
		return f, err
	}
	return f, nil
}

func removePid(f *os.File) error {
	f.Close()
	return os.Remove(f.Name())
}
